package com.magenta.maxoptra.gis;

import java.io.Serializable;
import java.util.Arrays;

/*
* Project: MaxOptRA v1.1
* User: Ivan Usalko
* Date: 19.01.2009 14:29:43
*
* Copyright (c) 1999-2009 Magenta Corporation Ltd. All Rights Reserved.
* Magenta Technology proprietary and confidential.
* Use is subject to license terms.
*/
/**
 * Unique key for all external geo-info requests.
 */
public class GeoInfoRequestId implements Serializable {

    static final long serialVersionUID = 5039788297484250734L;

    enum KeyType {
        COMPOUND(0x00), SIMPLE(0x01);
        private byte m;

        private KeyType(int m) {
            this.m = (byte) m;
        }

        static KeyType valueOf(int v) {
            for (KeyType type : KeyType.values()) {
                if (type.m == v) {
                    return type;
                }
            }
            return null;
        }
    }

    public static final int MAX_KEY_LENGTH = 1024;
    final byte[] id;

//    /**
//     * Generate simple key...
//     *
//     * @param settings    - scheduler instance.
//     * @param requestType - type of requestion.
//     * @param sites       - request params.
//     */
//    public GeoInfoRequestId(SchedulerRunSettings settings, GeoInfoRequestType requestType, Site... sites) {
//        byte[] compound = getKeyHeader(requestType);
//        //compound = compoundBytes(compound, getKey(settings.getUserId()));
//        //compound = compoundBytes(compound, getKey(settings.getCID()));
//        if (settings.getRoutingProvider() != null) {
//            compound = compoundBytes(compound, getKey(settings.getRoutingProvider()));
//        } else {
//            compound = compoundBytes(compound, getKey(settings.getDefaultRoutingProvider()));
//        }
//        //compound = compoundBytes(compound, ";".getBytes());
//        int rest = sites.length;
//        for (Site site : sites) {
//            byte[] key = getKey(site);
//            if (key.length > 0) {
//                compound = compoundBytes(compound, key);
//                //if (rest > 1) {
//                //    compound = compoundBytes(compound, ";".getBytes());
//                //}
//            }
//        }
//        this.id = compound;
//    }

//    /**
//     * Generate simple key...
//     *
//     * @param settings    scheduler instance.
//     * @param requestType type of requestion.
//     * @param params      additionalrequest params.
//     */
//    public GeoInfoRequestId(SchedulerRunSettings settings, GeoInfoRequestType requestType, byte[]... params) {
//        byte[] compound = getKeyHeader(requestType);
//        //compound = compoundBytes(compound, getKey(settings.getUserId()));
//        //compound = compoundBytes(compound, getKey(settings.getCID()));
//        if (settings.getRoutingProvider() != null) {
//            compound = compoundBytes(compound, getKey(settings.getRoutingProvider()));
//        } else {
//            compound = compoundBytes(compound, getKey(settings.getDefaultRoutingProvider()));
//        }
//        //compound = compoundBytes(compound, ";".getBytes());
//        for (byte[] param : params) {
//            if (param.length > 0) {
//                compound = compoundBytes(compound, cover(param));
//            }
//        }
//        this.id = compound;
//    }

//    /**
//     * Generate simple key...
//     *
//     * @param clazz       of geo info provider.
//     * @param requestType type of requestion.
//     * @param sites       additionalrequest params.
//     */
//    public GeoInfoRequestId(Class<? extends IGeoConfigurable> clazz, GeoInfoRequestType requestType, Site... sites) {
//        byte[] compound = getKeyHeader(requestType);
//        compound = compoundBytes(compound, getKey(clazz));
//        int rest = sites.length;
//        for (Site site : sites) {
//            byte[] key = getKey(site);
//            if (key.length > 0) {
//                compound = compoundBytes(compound, key);
//                //if (rest > 1) {
//                //    compound = compoundBytes(compound, ";".getBytes());
//                //}
//            }
//        }
//        this.id = compound;
//    }

    //

    /**
     * Generate simple key...
     *
     * @param providerName type of geo-info provider.
     * @param versionApi version API for access to external geo-info provider.
     * @param requestType   type of requestion.
     * @param requestParams request params.
     */
    public GeoInfoRequestId(String providerName, String versionApi, GeoInfoRequestType requestType, Object... requestParams) {
        byte[] compound = getKeyHeader(requestType);
        compound = compoundBytes(compound, providerName.getBytes());
        if (versionApi!= null) {
            compound = compoundBytes(compound, versionApi.getBytes());
        }
        for (Object p : requestParams) {
            byte[] key = getKey(p);
            if (key.length > 0) {
                compound = compoundBytes(compound, key);
            }
        }
        this.id = compound;
    }

    /**
     * Generate simple key...
     *
     * @param key - anyone key.
     */
    public GeoInfoRequestId(byte[] key) {
        this.id = key;
    }

//    /**
//     * Generate compound key...
//     *
//     * @param keys - anyone keys.
//     */
//    public GeoInfoRequestId(byte[]... keys) {
//        byte[] compound = new byte[]{KeyType.COMPOUND.m};
//        for (byte[] key : keys) {
//            if (key.length > 0) {
//                compound = compoundBytes(compound, cover(key));
//            }
//        }
//        this.id = compound;
//    }

    private static byte[] cover(byte[] key) {
        return compoundBytes(shortToBytes((short) key.length), key);
    }

    private static byte[] uncover(byte[] coveredKey) {
        int headerLen = 2;
        short keylen = bytesToShort(new byte[]{coveredKey[0], coveredKey[1]});
        byte[] uncoveredKey = new byte[keylen];
        System.arraycopy(coveredKey, headerLen, uncoveredKey, 0, keylen);
        return uncoveredKey;
    }

    private static byte[] shortToBytes(short s) {
        return new byte[]{(byte) (s & 0xff), (byte) ((s & 0xff00) >> 8)};
    }

    private static short bytesToShort(byte[] bytes) {
        return (short) ((bytes[0] & 0xff) + ((bytes[1] & 0xff) << 8));
    }

    private static byte[] compoundBytes(byte[] bytes1, byte[] bytes2) {
        int oldLength = bytes1.length;
        byte[] compound = Arrays.copyOf(bytes1, oldLength + bytes2.length);
        System.arraycopy(bytes2, 0, compound, oldLength, bytes2.length);
        return compound;
    }

//    private static String addLeadNZero(String str, int length) {
//        if (str.length() >= length) {
//            return str.substring(0, length);
//        }
//        StringBuffer buff = new StringBuffer(length);
//        for (int i = 0; i < (length - str.length()); i++) {
//            buff.append('0');
//        }
//        return buff.append(str).toString();
//    }
//
//    private static double periodical(double value, double period) {
//        double rest = value % period;
//        double factor = (int) value / (int) period;
//        if (factor > 0) {
//            return -period + rest;
//        } else if (factor < 0) {
//            return period + rest;
//        }
//        return rest;
//    }
//
//    private static String asLongitude(double lon) {
//        double longitude = periodical(lon, 180);
//        int radix = (int) longitude % 180;
//        int fraction = (int) Math.round((longitude - (int) longitude) * GeoPrecision.SIGN_FACTOR);
//        StringBuffer result = new StringBuffer(1 + 3 + GeoPrecision.COORDINATES_PRECISION);
//        result.append(longitude < 0 ? '-' : '+');
//        result.append(addLeadNZero(String.valueOf(Math.abs(radix)), 3));
//        result.append(addLeadNZero(String.valueOf(Math.abs(fraction)), GeoPrecision.COORDINATES_PRECISION));
//        return result.toString();
//    }
//
//    private static String asLatitude(double lat) {
//        double latitude = periodical(lat, 90);
//        int radix = (int) latitude % 90;
//        int fraction = (int) Math.round((latitude - (int) latitude) * GeoPrecision.SIGN_FACTOR);
//        StringBuffer result = new StringBuffer(1 + 3 + GeoPrecision.COORDINATES_PRECISION);
//        result.append(latitude < 0 ? '-' : '+');
//        result.append(addLeadNZero(String.valueOf(Math.abs(radix)), 2));
//        result.append(addLeadNZero(String.valueOf(Math.abs(fraction)), GeoPrecision.COORDINATES_PRECISION));
//        return result.toString();
//    }

    //byte[]
    //homeCoords, jobStartCoords, jobEndCoords
    //5 sign after zero ~ 1,2 m
//    public static byte[] getKey(Site site) {
//        //todo ivictus: add Densely packing or base15 coding for (Sites...) in constructor.
//        //byte[] key1 = String.valueOf(GeoPrecision.longitudeRound(site.getLongitude())).getBytes();
//        //byte[] key2 = String.valueOf(GeoPrecision.latitudeRound(site.getLatitude())).getBytes();
//        //byte[] result = compoundBytes(key1, ":".getBytes());
//        //result = compoundBytes(result, key2);
//        //return result;
//        double longitude = GeoPrecision.longitudeRound(site.getLongitude());
//        double latitude = GeoPrecision.latitudeRound(site.getLatitude());
//        return (asLatitude(latitude) + asLongitude(longitude)).getBytes();
//    }

    //

    public static byte[] getKey(Object obj) {
        if (obj instanceof GeographicCoordinates) {
            return getKey((GeographicCoordinates) obj);
        }
        if (obj instanceof Long) {
            return getKey((Long) obj);
        }
        if (obj instanceof Integer) {
            return getKey((Integer) obj);
        }
        if (obj instanceof String) {
            return getKey((String) obj);
        }
        if (obj instanceof Class) {
            return getKey((Class) obj);
        }
        throw new UnsupportedOperationException("Unimplemented code!");
    }

    private static byte[] twoFirstChar(String s) {
        if (s.length() > 2) {
            return s.substring(0, 2).getBytes();
        } else if (s.length() == 1) {
            return (s + "_").getBytes();
        } else {
            return "??".getBytes();
        }
    }

    public static byte[] getKey(GeographicCoordinates site) {
        //todo ivictus: add Densely packing or base15 coding for (Sites...) in constructor.
        double longitude = GeoPrecision.longitudeRound(site.getLongitude());
        double latitude = GeoPrecision.latitudeRound(site.getLatitude());
        return (asLatitude(latitude) + asLongitude(longitude)).getBytes();
    }

    private static String asLongitude(double lon) {
        double longitude = periodical(lon, 180);
        int radix = (int) longitude % 180;
        int fraction = (int) Math.round((longitude - (int) longitude) * GeoPrecision.SIGN_FACTOR_LON);
        StringBuffer result = new StringBuffer(1 + 3 + GisConstants.MAX_LONGITUDE_ACCURACY);
        result.append(longitude < 0 ? '-' : '+');
        result.append(addLeadNZero(String.valueOf(Math.abs(radix)), 3));
        result.append(addLeadNZero(String.valueOf(Math.abs(fraction)), GisConstants.MAX_LONGITUDE_ACCURACY));
        return result.toString();
    }

    private static String asLatitude(double lat) {
        double latitude = periodical(lat, 90);
        int radix = (int) latitude % 90;
        int fraction = (int) Math.round((latitude - (int) latitude) * GeoPrecision.SIGN_FACTOR_LAT);
        StringBuffer result = new StringBuffer(1 + 3 + GisConstants.MAX_LATITUDE_ACCURACY);
        result.append(latitude < 0 ? '-' : '+');
        result.append(addLeadNZero(String.valueOf(Math.abs(radix)), 2));
        result.append(addLeadNZero(String.valueOf(Math.abs(fraction)), GisConstants.MAX_LATITUDE_ACCURACY));
        return result.toString();
    }

    private static String addLeadNZero(String str, int length) {
        if (str.length() >= length) {
            return str.substring(0, length);
        }
        StringBuffer buff = new StringBuffer(length);
        for (int i = 0; i < (length - str.length()); i++) {
            buff.append('0');
        }
        return buff.append(str).toString();
    }

    private static double periodical(double value, double period) {
        double rest = value % period;
        double factor = (int) value / (int) period;
        if (factor > 0) {
            return -period + rest;
        } else if (factor < 0) {
            return period + rest;
        }
        return rest;
    }

    public static byte[] getKey(String name) {
        //TODO: maybe two symbols too litle?
        return twoFirstChar(name);
    }

    public static byte[] getKey(Class clazz) {
        //TODO: maybe two symbols too litle?
        return twoFirstChar(clazz.getSimpleName());
    }

    //byte[]
    public static byte[] getKey(Long aLong) {
        long v = aLong != null ? aLong : -1;
        return new byte[]{(byte) ((v >> 7) & 0xff), (byte) ((v >> 6) & 0xff), (byte) ((v >> 5) & 0xff),
                (byte) ((v >> 4) & 0xff), (byte) ((v >> 3) & 0xff), (byte) ((v >> 2) & 0xff),
                (byte) ((v >> 1) & 0xff), (byte) (v & 0xff)};
    }

//    public static Long getLong(byte[] key) throws ParseException {
//        if (key.length > 7) {
//            Long l = (((long) key[0]) << 7) | (((long) key[1]) << 6) | (((long) key[2]) << 5) | (((long) key[3]) << 4) |
//                    (((long) key[4]) << 3) | (((long) key[5]) << 2) | (((long) key[6]) << 1) | ((long) key[7]);
//            if (l == -1) {
//                return null;
//            }
//            return l;
//        }
//        throw new ParseException();
//    }

    public static byte[] getKey(Integer i) {
        int v = i != null ? i : -1;
        return new byte[]{(byte) ((v >> 3) & 0xff), (byte) ((v >> 2) & 0xff),
                (byte) ((v >> 1) & 0xff), (byte) (v & 0xff)};
    }

//    /**
//     * If key is compound then, return all included keys.
//     * Otherwise method return self key.
//     *
//     * @return one or more keys.
//     */
//    public List<GeoInfoRequestId> getKeys() {
//        switch (KeyType.valueOf(id[0])) {
//            case COMPOUND:
//                List<GeoInfoRequestId> keys = new ArrayList<GeoInfoRequestId>();
//                int idx = 1;
//                int headerLen = 2;
//                while (idx < id.length) {
//                    byte[] buf = new byte[id.length - idx];
//                    System.arraycopy(id, idx, buf, 0, id.length - idx);
//                    byte[] key = uncover(buf);
//                    keys.add(new GeoInfoRequestId(key));
//                    idx += key.length + headerLen;
//                }
//                return keys;
//            case SIMPLE:
//                return Collections.singletonList(this);
//        }
//        throw new UnsupportedOperationException("Unimplemented!");
//    }

    public byte[] getBytes() {
        return id;
    }

    public String encode() {
        return encode(this);
    }

    private static byte[] getKeyHeader(GeoInfoRequestType requestType) {
        return new byte[]{KeyType.SIMPLE.m, (byte) requestType.ordinal()};
    }

    public static GeoInfoRequestId decode(String data) {
        byte[] keyHeader = getKeyHeader(GeoInfoRequestType.FASTEST_ROUTE);
        byte[] keyBody = data.getBytes();
        byte[] key = Arrays.copyOf(keyHeader, keyHeader.length + keyBody.length);
        System.arraycopy(keyBody, 0, key, keyHeader.length, keyBody.length);
        return new GeoInfoRequestId(key);
    }

    private static int SIMPLE_KEY_HEADER_SIZE = getKeyHeader(GeoInfoRequestType.FASTEST_ROUTE).length;

    public static String encode(GeoInfoRequestId data) {
        if (data.id.length > SIMPLE_KEY_HEADER_SIZE) {
            byte[] keyBody = new byte[data.id.length - SIMPLE_KEY_HEADER_SIZE];
            System.arraycopy(data.id, SIMPLE_KEY_HEADER_SIZE, keyBody, 0, keyBody.length);
            return new String(keyBody);
        }
        return "";
    }

//    public static void main(String[] args) {
//
//        //TEST asLatitude()
//        System.out.println(asLatitude(-180.33435));
//        System.out.println(asLatitude(90.123));
//        System.out.println(asLatitude(91.123));
//        System.out.println(asLatitude(-1.123));
//        System.out.println(asLatitude(-1.0123));
//        System.out.println(asLatitude(-91.123));
//        System.out.println(asLatitude(3600.298763));
//        System.out.println(asLatitude(51.0575));
//        System.out.println(asLatitude(51.0922));
//        //TEST asLongitude()
//        System.out.println(asLongitude(-1.7263));
//        System.out.println(asLongitude(101.1234));
//        System.out.println(asLongitude(-101.1235));
//        System.out.println(asLongitude(-101.12351));
//        System.out.println(asLongitude(290.33435));
//        System.out.println(asLongitude(-180.33435));
//
//        //TEST twoFirstChar()
//        System.out.println(new String(twoFirstChar("GEOINFO")));//GE
//        System.out.println(new String(twoFirstChar("G")));//G_
//        System.out.println(new String(twoFirstChar("")));//??
//
//        Site s = new Site();
//        s.setLatitude(50.12345);
//        s.setLongitude(53.12345);
//        byte[] result = GeoInfoRequestId.getKey(s);
//
//        System.out.println(result.toString());
//        //test -- bytesToShort and shortToBytes
//        System.out.println(bytesToShort(shortToBytes((short) 1234)));
//        //test -- cover and uncover
//        System.out.println(uncover(cover(new byte[]{100, 101})));
//
//        //test -- compounding...
//        Site home = new Site();
//        home.setLongitude(50.12345);
//        home.setLatitude(53.12345);
//
//        Site a = new Site();
//        a.setLongitude(51.12345);
//        a.setLatitude(52.12345);
//
//        Site b = new Site();
//        b.setLongitude(50.12345);
//        b.setLatitude(52.12345);
//
//        GeoInfoRequestType reqType = GeoInfoRequestType.FASTEST_ROUTE;
//
//        SchedulerRunSettings clientID = new SchedulerRunSettingsAbstract(-1, 0L, null) {
//            @Override
//            public Class<? extends IGeoRouting> getRoutingProvider() {
//                return GeocoderGoogleImpl.class;
//            }
//        };
//
//        //TEST constructors...
//        System.out.println(
//                new GeoInfoRequestId(new GeoInfoRequestId(clientID, reqType, home, a).getBytes(),
//                        new GeoInfoRequestId(clientID, reqType, a, b).getBytes(),
//                        new GeoInfoRequestId(clientID, reqType, b, home).getBytes()).getKeys());
//
//        //TEST encode and decode methods...
//        System.out.println(GeoInfoRequestId.decode("AAA"));
//        System.out.println(GeoInfoRequestId.encode(GeoInfoRequestId.decode("AAA")));
//    }

    @Override
    public boolean equals(Object obj) {
        return obj instanceof GeoInfoRequestId && Arrays.equals(((GeoInfoRequestId) obj).id, id);
    }

    @Override
    public int hashCode() {
        return Arrays.hashCode(id);
    }

    @Override
    public String toString() {
        return super.toString() + " : " + new String(id);
    }
}
